[ 筆記 ] JavaScript - 02 函式


Posted by krebikshaw on 2020-06-22

函式的定義

JavaScript 函式定義有 3 種方式,分別在效率、解析順序、作用域有不同的特性。

// 1. 語句函式
function fun1(){  };

// 2. 函式賦值變數
var test = function(){  };

// 3. 建構式函數
var test2 = new Function();

編譯順序

JavaScript 在執行程式之前,會先對 語句函式 進行解析,再去一行一行執行程式,所以第一種 語句函式,在被呼叫之前,就會先解析好並儲存在內存中。

  • 以第一種宣告方式,由於函式會先行解析好,所以若是把呼叫函式的指令,寫在函式的程式碼上面,是可以成功呼叫到函式的。
  • 以第二種宣告方式 函式賦值變數,系統會先判讀有宣告變數 test 但還不會賦值。只有執行到該行程式碼時,才會把函式賦值給變數。所以若是把呼叫函式的指令,寫在函式的程式碼上面,系統會判斷為 undefined
fun1()
// 第一種宣告方式,可以先呼叫函式,再宣告。
function fun1() {  };

test()
// 第二種宣告方式,會判斷 test is not a function
var test = function() {  };

解析範例

看看以下經典範例來說明解析步驟:

  1. JavaScript解析器,會首先找到第一個test1( ) → 1
  2. 接下來找到第四個test1( ),並蓋掉第一個test1( ) → 4
  3. 再後來沒有function開頭語句了,所以才從頭一行一行執行並回傳
  4. 最後依序回傳 4, 2, 3, 3, 5, 6
function test1() { return 1; }
alert(test1());  // 4 → 被第四個 test1() 蓋掉

var test1 = function() { return 2; };
alert(test1());  // 2 

var test1 = new Function("return 3");
alert(test1()); // 3

function test1() { return 4; }
alert(test1()); // 3 → 第四個已經被解析過了所以跳過

var test1 = function() { return 5; };
alert(test1()); // 5

var test1 = new Function("return 6");
alert(test1()); // 6

執行效率

以效率方面來比較,相同的執行內容,所需的執行時間,大約是 1 = 2 < 3

  • 第一種及第二種方式,由於編譯完成後就會放在內存,使用時再呼叫,所以很適合多次呼叫的情形。
  • 第三種方式 建構式函式,使用後會被釋放掉,不會佔用內存,所以再一次呼叫時,要重新 new 一次,僅適合一次性呼叫。

作用域

  • 第一、二種方法具有函式作用域,return 時會得到宣告在函式內部的區域變數
  • 第三種 建構函式 方法具有頂級作用域,在函式中宣告的變數,會屬於全域變數。所以 return 時也會去抓全域變數的值。(這部分不是很理解,先暫時記住此特性)
var k = 1; //全域變數

function fun(){
  var k = 2; //區域變數

  // 函式作用域底下 (return 時會回傳 2)-----
  function test(){ return k; }
  var test = function() { return k; };

  // 頂級作用域底下 (return 時會回傳 1)-----
  var test = new Function("return k;");

  alert(test());
}

fun();

三種宣告方式比較

類型 語句函式 函式賦值變數 建構式函式
名稱 具名 匿名 匿名 |
性質 靜態 靜態 動態
解析時機 優先解析 順序解析 順序解析
多次呼叫效率
作用域 函式作用域 函式作用域 頂級作用域

調用函式

  • 這部分現在看不懂,先記下來,以後可能會用到
    執行函式,是在函式被調用時才開始,非函式定義時,Javascript有四種調用方式
  1. 作為函式(function)
  2. 作為方法(methods)
  3. 作為建構式(constructors)
  4. 透過 cell() 與 apply() 方法間接調用
    方法調用與函式調用有一個重要的差異:調用情境,即函式主體的 this 會視呼叫它的物件而定:
  • 當方法:其調用物件
  • 當函式:
    • strict 模式:全域物件
    • 非 strict 模式:undifined

return

函式中的 return 指令會使函式停止執行,並回傳運算結果(如果有的話),如果 return 的回傳值沒有東西,就會回傳 undifined。

  • 並非所有函式都有 return

選擇性參數

函式調用的 引數比參數少 時,額外參數會被設為 undifined,我們可以為 可能不存在 的參數設定 預設值

非必須的引數,需要寫在引數列的後面。

var obj1 = {a:"1", b:"2", c:"3"};
var obj2 = {d:"1", e:"2", f:"3"};

function getPropertyNames(o,a) { // o → 必須 ; a → 可以忽略
  a = a || []; // 如果a不存在就建一個新陣列
  for(var property in o) a.push(property); // 把o的特性值丟進a陣列
  return a;

}

var newObj = getPropertyNames(obj1); // 把obj1特性丟進新建的newObj
getPropertyNames(obj2, newObj); // 把obj2特性也丟進newObj

console.log(newObj); // ["a", "b", "c", "d", "e", "f"]

引數 Argument

JavaScript 提供了一個很方便的指令,可以讓我們查看傳入函式的 引數 是什麼。

  • Argument 是一種類陣列物件,可以用 length 來看長度,實際上 log 出來看,它其實是一個物件形式。
// 抓出最大值
Max(1,5,100,20); // 100

function Max() {
  var max = 0;
  var num = arguments; // 把引數陣列丟進num
  // num.length → 4
  // num[0] → 1 ; num[1] → 5; num[2] → 100...

  for (var i=0; i<num.length; i++) {
    if (max < num[i]) max = num[i]; // 當前元素大於max時,max更新
  }
  return max;
}

使用物件作為引數

當函式的參數比較多,可以利用物件 key: value 的特性,讓函式直接接收一個物件。

var a = [1,2,3,4], b = [];
copy({from:a, to:b, length:4});

function copy(obj) {
    // obj.from → [1, 2, 3, 4]
    // obj.form_start || 30 → 30
    // obj.to → []
    // obj.to_start: obj.to_start || 20 → 20
    // obj.length → 4
}

函式作為值

  • 函式也可以當做「值」,這代表他們可以被指定給變數、物件特性或陣列元素中,或是傳給函式當作引數用。
// 計算式
function add(x,y) { return x+y; }
function subtract(x,y) {return x-y; }

// 調用上面運算式做為第一個引數
function operate(method, num1, num2) {
  return method(num1, num2);
}

// a = 5+10
var a = operate(add, 5, 10);

// c = (10-5) + (2-1)
var c = operate(add, 
operate(subtract, 10, 5), 
operate(subtract, 2, 1));

函式的物件特性

函式是一種物件,代表也有特性可以存取,假設你要寫一個每次調用都回傳不同值的函式,且都要記錄上次存取的值,我們可以把資訊直接存在 函式特性

addCount.count = 0;
function addCount() {
  return addCount.count++;
}

addCount();
addCount();
addCount();

console.log( addCount.count ); // 3

call & apply方法

.call().apply() 可以用來傳遞參數用,但更重要的功能是用來間接調用某函式,當作是某物件的方法。

  • 第一個引數是指出要 在哪個物件上調用,在函式內容中成為this
    • call:任何多個引數 fun.call(object, num1, num2)
    • apply:一組陣列當引數 fun.apply(object, [num1, num2])
//傳遞參數用--------------
function sum(x,y) {
  return x+y;
}

// call 
function callTest(num1, num2) {
  return sum.call(this, num1, num2);
}
// apply 
function applyTest(num1, num2) {
  return sum.apply(this, [num1, num2]);
}
/* apply 也可以改寫用 arguments
function applyTest() {
  return sum.apply(this, arguments);
}
*/

callTest(10,20);  //30
applyTest(10,50); //60

間接調用函式

//間接調用函式,當做是物件的方法--------------
function sum(x,y) {
  return x+y;
}

function Obj(x,y) {
  this.x = x;
  this.y = y;
}

var o = new Obj();

//方法.apply(作用域, [參數陣列])
sum.apply(o, [10,100]); // 110

Closures 閉包

函式執行完畢後,變數會被釋放掉,但是我們可以利用在函式中宣告另一個函式,並把它 return 出去,來 keep 住我們要保留的變數資料

EC: Execution Context,每個進入一個 工作 BLOCK,會產生一個新的 EC,每次進入一個新的 EC,Scope chain 就會被建立。
VO: Variable Object,在建立 Global EC 的時候,用來紀錄初始化變數的地方。

Global EC: {
    VO: {
        str: undefined
        fun1: function
    }
}

AO: Activation Object,用來在新增 function EC 的時候,紀錄初始化變數的地方。跟 Variable Object 很像,差別只在 AO 是在新稱 function EC 的時候產生。

function EC: {
    AO: {
        arr: undefined
        fun2: function
    }
    scopeChain: [function EC.AO, [Scope]]
}

Block 運作方式:

// 請先記住這段程式碼!! 下面會針對這段程式碼來做說明
var a = 1
function test() {
    var b = 2
    function inner() {
        var c = 3
        console.log(b)
        console.log(a)
    }
    return inner()
}
test()

模擬 JS 引擎:

  1. 程式開始執行,產生 Global EC,並初始化 VO,建立自己的 ScopeChain
    Global EC: {
        VO: {
            a: undefined
            test: function
        }
        ScopeChain: [global EC.VO]
    }
    // test.[[scope]] 初始化
    test.[[Scope]] = global EC.ScopeChain  // global EC.VO
    
  2. 開始跑程式,跑到 var a=1 的時候,將 global EC.VO.a 改成 1
  3. 進入 test function 之後,產生一個 test EC 並初始化 AO
    test EC: {
       AO: {
           b: undefined
           inner: function
       }
       ScopeChain: [test EC.AO, test.[[scope]]]
       // test EC.AO, global EC.ScopeChain
    }
    // inner.[[scope]] 初始化
    inner.[[scope]] = test EC.ScopeChain // test EC.AO, global EC.ScopeChain
    
  4. 執行到 var b=2 的時候,將 test EC.AO.b 改成 2
  5. 進入 inner 之後,產生一個 inner EC,初始化 AO
    inner EC: {
       AO: {
           c: undefined
       }
       ScopeChain: [inner EC.AO, inner.[[scope]]]
       // inner EC.AO, test EC.ScopeChain
       // inner EC.AO, test EC.AO, global EC.VO
    }
    
  6. 執行到 var c=3 的時候,將 inner EC.AO.c 改成 3
  7. 執行 console.log(b) 會先在 inner EC.AO 找,找不到再去 test EC.AO 找,找到了 b,就 log 出 b = 2
  8. 執行 console.log(a) 會先在 inner EC.AO 找,找不到再去 test EC.AO 找,找不到再去 global EC.VO 找,找到了,就 log 出 a = 1

ScopeChain 說穿了就是從自己所在的 Executin Context,開始一路把到 global EC 過程中的 AO 及 VO 串通通起來。

因為內部的函式,它的 ScopeChain 含有外層函式的 AO,所以內部函式被 return 出去,系統不能把外層函式的 AO 釋放掉,因而保留了變數的資料。

Closure 就是函式結束後要將其釋放,卻因為有保留 ScopeChain,所以即便已經結束函式了,還是可以存取到函式裡的值。

參考資料:JavaScript大全(第六版)


#javascript #函式 #function







Related Posts

MarkDown語法

MarkDown語法

[Math] 如何於CoderBridge撰寫數學公式

[Math] 如何於CoderBridge撰寫數學公式

Go起手式之四

Go起手式之四


Comments